/*
 * Copyright 2011 Michele Mancioppi [michele.mancioppi@gmail.com]
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cave.nice.testMessage.mail;

import java.io.IOException;
import java.util.Date;
import java.util.Properties;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import javax.mail.Address;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMessage.RecipientType;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;

import cave.nice.testMessage.TestMessageConstants;
import cave.nice.testMessage.data.CannotUpdateEntityException;
import cave.nice.testMessage.data.DataManager;
import cave.nice.testMessage.data.DataManagerException;
import cave.nice.testMessage.data.Test;
import cave.nice.testMessage.data.TestAlreadyAnsweredException;
import cave.nice.testMessage.data.UknownTestChallengeException;
import cave.nice.testMessage.data.UnknownVerifiedAccountException;
import cave.nice.testMessage.data.UnprocessedEMail;
import cave.nice.testMessage.data.VerifiedAccount;
import cave.nice.testMessage.data.WrongAccountForTestException;

import com.google.appengine.api.datastore.KeyFactory;

/**
 * Processes the incoming messages. They are supposed to be test answers. If
 * not, make a fuss.
 * 
 * @author Michele
 */
@SuppressWarnings("serial")
public class MailHandlerServlet extends HttpServlet {

	private static final Logger LOGGER = Logger
			.getLogger(MailHandlerServlet.class.getName());

	private InternetAddress appInternetAddress = null;

	@Override
	public void init(ServletConfig config) throws ServletException {
		try {
			appInternetAddress = new InternetAddress(
					TestMessageConstants.TESTS_EMAIL_ADDRESS, true);
		} catch (AddressException e) {
			throw new ServletException(
					"Error while creating the app internet address using the email '"
							+ TestMessageConstants.TESTS_EMAIL_ADDRESS + "'", e);
		}
	}

	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		DataManager dataStoreManager = DataManager.getInstance();

		try {
			Properties props = new Properties();
			Session session = Session.getDefaultInstance(props, null);
			MimeMessage message = null;
			try {
				message = new MimeMessage(session, req.getInputStream());
			} catch (MessagingException e) {
				throw new ServletException(
						"Error while retrieving an incoming mail", e);
			}
			Address[] froms = null;
			try {
				froms = message.getFrom();
			} catch (MessagingException e) {
				throw new ServletException(
						"Error while retrieving an the 'from' field from the following message: "
								+ message, e);
			}
			InternetAddress from = null;
			switch (froms.length) {
			case 0: {
				/*
				 * No from address? How are we supposed to check the validity of
				 * the test?
				 * 
				 * TODO Check if the email has at least a valid test identifier
				 * inside. If not, figure out something
				 */
				throw new ServletException(
						"The following message has no 'from' field set: "
								+ message);
			}
			case 1: {
				/*
				 * That's how we like it ;-)
				 */
				from = (InternetAddress) froms[0];
				break;
			}
			default: {
				/*
				 * Uhm... multiple 'from'? TODO Check for a 'from' we know, and
				 * assume that. Plus, log a warning.
				 */
				throw new ServletException(
						"The following message has multiple 'from' fields set: "
								+ message);
			}

			}
			try {
				if (message
						.getHeader(TestMessageConstants.SMTP_HEADER_TEST_IDENTIFIER) != null
						|| message
								.getHeader(TestMessageConstants.SMTP_HEADER_ACCOUNT_IDENTIFIER) != null) {
					processTestAnswerInHeaders(dataStoreManager, message);
				} else {
					processTestAnswerInMessageBody(dataStoreManager, message,
							from);
				}
			} catch (DataManagerException e) {
				throw new ServletException(e.getMessage());
			} catch (Exception e) {
				UnprocessedEMail em = dataStoreManager
						.storeUnprocessedEMail(message);
				LOGGER.log(Level.SEVERE,
						"Incoming message could not be processed, saved as UnprocessedEMail "
								+ KeyFactory.keyToString(em.getIdentifier()), e);
			}
		} finally {
			dataStoreManager.close();
		}
	}

	private void processTestAnswerInHeaders(DataManager dataManager,
			MimeMessage message) throws ServletException,
			UknownTestChallengeException, UnknownVerifiedAccountException,
			WrongAccountForTestException, CannotUpdateEntityException {
		try {
			String[] testIdentifiers = message
					.getHeader(TestMessageConstants.SMTP_HEADER_TEST_IDENTIFIER);
			if (testIdentifiers.length == 0) {
				throw new ServletException("Required SMTP header '"
						+ TestMessageConstants.SMTP_HEADER_TEST_IDENTIFIER
						+ "' not found");
			} else if (testIdentifiers.length > 1) {
				throw new ServletException(
						"More than one value for the header '"
								+ TestMessageConstants.SMTP_HEADER_TEST_IDENTIFIER
								+ "' found");
			}

			String[] accountIdentifiers = message
					.getHeader(TestMessageConstants.SMTP_HEADER_ACCOUNT_IDENTIFIER);
			if (accountIdentifiers.length == 0) {
				throw new ServletException("Required SMTP header '"
						+ TestMessageConstants.SMTP_HEADER_ACCOUNT_IDENTIFIER
						+ "' not found");
			} else if (accountIdentifiers.length > 1) {
				throw new ServletException(
						"More than one value for the header '"
								+ TestMessageConstants.SMTP_HEADER_ACCOUNT_IDENTIFIER
								+ "' found");
			}

			String testChallenge = testIdentifiers[0];
			UUID challenge = UUID.fromString(testChallenge);
			String accountIdentifier = accountIdentifiers[0];

			VerifiedAccount account = dataManager.getVerifiedAccount(KeyFactory
					.stringToKey(accountIdentifier));

			processSMTPTestAnswer(dataManager, account, challenge);
		} catch (MessagingException e) {
			throw new ServletException(
					"Error while accessing the headers of the message", e);
		} catch (TestAlreadyAnsweredException e) {
			/*
			 * TODO Error message
			 */
			throw new ServletException(e);
		}

	}

	private void processTestAnswerInMessageBody(DataManager dataStoreManager,
			MimeMessage message, InternetAddress from) throws ServletException,
			IOException, UnknownVerifiedAccountException,
			WrongAccountForTestException, CannotUpdateEntityException,
			UknownTestChallengeException, TestAlreadyAnsweredException {
		/*
		 * It could be that the address is the one used to send the test in the
		 * first place :D
		 */
		VerifiedAccount account = null;
		if (from.equals(appInternetAddress)) {
			/*
			 * OK, they just forwarded it.
			 */
			Address[] tos = null;
			try {
				tos = message.getRecipients(RecipientType.TO);
			} catch (MessagingException e) {
				throw new ServletException(
						"Cannot access TO's of the message: " + message);
			}

			if (tos != null && tos.length > 0) {
				for (Address to : tos) {
					if (!(to instanceof InternetAddress)) {
						/*
						 * Well, if this is the case, we want nothing to do with
						 * this address :D
						 */
						continue;
					}

					try {
						account = dataStoreManager
								.getVerifiedAccount((InternetAddress) to);
						break;
					} catch (UnknownVerifiedAccountException e) {
						/*
						 * We don't know this. Strange, but let's move on
						 */
					}
				}
			}

			if (account == null) {
				throw new ServletException("Message to unknown accounts '"
						+ tos + "': " + message);
			}
		} else {
			/*
			 * It is not a forward... Could it be a reply?
			 */
			try {
				account = dataStoreManager.getVerifiedAccount(from);
			} catch (UnknownVerifiedAccountException e) {
				throw new ServletException("Message from unknown account '"
						+ from + "': " + message);
			}
		}

		/*
		 * OK, we know the account. Now, do we recognize the test that is
		 * answered?
		 */
		String body = null;
		try {
			body = IOUtils.toString(message.getInputStream());
			body = body.replaceAll("\\s", "");
		} catch (MessagingException e) {
			throw new ServletException("Cannot access content of the message:"
					+ message);
		}

		/*
		 * Remove from body the white spaces: if one has been inserted in the
		 * id, we would not recognize it
		 */
		Matcher matcher = null;
		String pattern = ".*#####(.+)#####.*";
		try {
			matcher = Pattern.compile(pattern).matcher(body);
		} catch (PatternSyntaxException e) {
			throw new ServletException("Cannot compile pattern: " + pattern, e);
		}

		if (!matcher.matches()) {
			throw new ServletException("No test identifier found in the body: "
					+ body);
		}

		String testChallenge = matcher.group(1);
		UUID challenge = UUID.fromString(testChallenge);

		processSMTPTestAnswer(dataStoreManager, account, challenge);
	}

	private void processSMTPTestAnswer(DataManager dataStoreManager,
			VerifiedAccount account, UUID testChallenge)
			throws UknownTestChallengeException,
			UnknownVerifiedAccountException, WrongAccountForTestException,
			CannotUpdateEntityException, TestAlreadyAnsweredException {
		Test openTest = dataStoreManager.getTestOfAccount(account,
				testChallenge);

		LOGGER.info("SMTP answer to the test '"
				+ KeyFactory.keyToString(openTest.getIdentifier())
				+ "' of the email account '"
				+ openTest.getAccount().getEmailAddress() + "'");

		dataStoreManager.markTestAsAnswered(openTest, new Date());
	}

}
